/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002-2006
* Sleepycat Software. All rights reserved.
*
* $Id: JoinCursor.java,v 1.1 2006/05/06 08:59:37 ckaestne Exp $
*/
package com.sleepycat.je;
import java.util.Arrays;
import java.util.Comparator;
import java.util.logging.Level;
import com.sleepycat.je.dbi.GetMode;
import com.sleepycat.je.dbi.CursorImpl.SearchMode;
import com.sleepycat.je.txn.Locker;
/**
* Javadoc for this public class is generated
* via the doc templates in the doc_src directory.
*/
public class JoinCursor {
private JoinConfig config;
private Database priDb;
private Cursor priCursor;
private Cursor[] secCursors;
private DatabaseEntry[] cursorScratchEntries;
private DatabaseEntry scratchEntry;
/**
* Creates a join cursor without parameter checking.
*/
JoinCursor(Locker locker,
Database primaryDb,
final Cursor[] cursors,
JoinConfig configParam)
throws DatabaseException {
priDb = primaryDb;
config = (configParam != null) ? configParam.cloneConfig()
: JoinConfig.DEFAULT;
scratchEntry = new DatabaseEntry();
cursorScratchEntries = new DatabaseEntry[cursors.length];
Cursor[] sortedCursors = new Cursor[cursors.length];
System.arraycopy(cursors, 0, sortedCursors, 0, cursors.length);
if (!config.getNoSort()) {
/*
* Sort ascending by duplicate count. Collect counts before
* sorting so that countInternal() is called only once per cursor.
* Use READ_UNCOMMITTED to avoid blocking writers.
*/
final int[] counts = new int[cursors.length];
for (int i = 0; i < cursors.length; i += 1) {
counts[i] = cursors[i].countInternal
(LockMode.READ_UNCOMMITTED);
assert counts[i] >= 0;
}
Arrays.sort(sortedCursors, new Comparator() {
public int compare(Object o1, Object o2) {
int count1 = -1;
int count2 = -1;
/*
* Scan for objects in cursors not sortedCursors since
* sortedCursors is being sorted in place.
*/
for (int i = 0; i < cursors.length &&
(count1 < 0 || count2 < 0); i += 1) {
if (cursors[i] == o1) {
count1 = counts[i];
} else if (cursors[i] == o2) {
count2 = counts[i];
}
}
assert count1 >= 0 && count2 >= 0;
return (count1 - count2);
}
});
}
/*
* Open and dup cursors last. If an error occurs before the
* constructor is complete, close them and ignore exceptions during
* close.
*/
try {
priCursor = new Cursor(priDb, locker, null);
secCursors = new Cursor[cursors.length];
for (int i = 0; i < cursors.length; i += 1) {
secCursors[i] = new Cursor(sortedCursors[i], true);
}
} catch (DatabaseException e) {
close(e); /* will throw e */
}
}
/**
* Javadoc for this public method is generated via
* the doc templates in the doc_src directory.
*/
public void close()
throws DatabaseException {
if (priCursor == null) {
throw new DatabaseException("Already closed");
}
close(null);
}
/**
* Close all cursors we own, throwing only the first exception that occurs.
*
* @param firstException an exception that has already occured, or null.
*/
private void close(DatabaseException firstException)
throws DatabaseException {
if (priCursor != null) {
try {
priCursor.close();
} catch (DatabaseException e) {
if (firstException == null) {
firstException = e;
}
}
priCursor = null;
}
for (int i = 0; i < secCursors.length; i += 1) {
if (secCursors[i] != null) {
try {
secCursors[i].close();
} catch (DatabaseException e) {
if (firstException == null) {
firstException = e;
}
}
secCursors[i] = null;
}
}
if (firstException != null) {
throw firstException;
}
}
/**
* For unit testing.
*/
Cursor[] getSortedCursors() {
return secCursors;
}
/**
* Javadoc for this public method is generated via
* the doc templates in the doc_src directory.
*/
public Database getDatabase() {
return priDb;
}
/**
* Javadoc for this public method is generated via
* the doc templates in the doc_src directory.
*/
public JoinConfig getConfig() {
return config.cloneConfig();
}
/**
* Javadoc for this public method is generated via
* the doc templates in the doc_src directory.
*/
public OperationStatus getNext(DatabaseEntry key,
LockMode lockMode)
throws DatabaseException {
priCursor.checkEnv();
DatabaseUtil.checkForNullDbt(key, "key", false);
priCursor.trace(Level.FINEST, "JoinCursor.getNext(key): ", lockMode);
return retrieveNext(key, null, lockMode);
}
/**
* Javadoc for this public method is generated via
* the doc templates in the doc_src directory.
*/
public OperationStatus getNext(DatabaseEntry key,
DatabaseEntry data,
LockMode lockMode)
throws DatabaseException {
priCursor.checkEnv();
DatabaseUtil.checkForNullDbt(key, "key", false);
DatabaseUtil.checkForNullDbt(data, "data", false);
priCursor.trace(Level.FINEST, "JoinCursor.getNext(key,data): ",
lockMode);
return retrieveNext(key, data, lockMode);
}
/**
* Internal version of getNext(), with an optional data param.
* <p>
* Since duplicates are always sorted and duplicate-duplicates are not
* allowed, a natural join can be implemented by simply traversing through
* the duplicates of the first cursor to find candidate keys, and then
* looking for each candidate key in the duplicate set of the other
* cursors, without ever reseting a cursor to the beginning of the
* duplicate set.
* <p>
* This only works when the same duplicate comparison method is used for
* all cursors. We don't check for that, we just assume the user won't
* violate that rule.
* <p>
* A future optimization would be to add a SearchMode.BOTH_DUPS operation
* and use it instead of using SearchMode.BOTH. This would be the
* equivalent of the undocumented DB_GET_BOTHC operation used by DB core's
* join() implementation.
*/
private OperationStatus retrieveNext(DatabaseEntry keyParam,
DatabaseEntry dataParam,
LockMode lockMode)
throws DatabaseException {
outerLoop: while (true) {
/* Process the first cursor to get a candidate key. */
Cursor secCursor = secCursors[0];
DatabaseEntry candidateKey = cursorScratchEntries[0];
OperationStatus status;
if (candidateKey == null) {
/* Get first duplicate at initial cursor position. */
candidateKey = new DatabaseEntry();
cursorScratchEntries[0] = candidateKey;
status = secCursor.getCurrentInternal(scratchEntry,
candidateKey,
lockMode);
} else {
/* Already initialized, move to the next candidate key. */
status = secCursor.retrieveNext(scratchEntry, candidateKey,
lockMode,
GetMode.NEXT_DUP);
}
if (status != OperationStatus.SUCCESS) {
/* No more candidate keys. */
return status;
}
/* Process the second and following cursors. */
for (int i = 1; i < secCursors.length; i += 1) {
secCursor = secCursors[i];
DatabaseEntry secKey = cursorScratchEntries[i];
if (secKey == null) {
secKey = new DatabaseEntry();
cursorScratchEntries[i] = secKey;
status = secCursor.getCurrentInternal(secKey, scratchEntry,
lockMode);
assert status == OperationStatus.SUCCESS;
}
scratchEntry.setData(secKey.getData(), secKey.getOffset(),
secKey.getSize());
status = secCursor.search(scratchEntry, candidateKey, lockMode,
SearchMode.BOTH);
if (status != OperationStatus.SUCCESS) {
/* No match, get another candidate key. */
continue outerLoop;
}
}
/* The candidate key was found for all cursors. */
if (dataParam != null) {
status = priCursor.search(candidateKey, dataParam,
lockMode, SearchMode.SET);
if (status != OperationStatus.SUCCESS) {
throw new DatabaseException("Secondary corrupt");
}
}
keyParam.setData(candidateKey.getData(), candidateKey.getOffset(),
candidateKey.getSize());
return OperationStatus.SUCCESS;
}
}
}